package cn.swust.HuXinMeng.sound;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.Mixer;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.UnsupportedAudioFileException;

import cn.swust.HuXinMeng.util.LoopingByteInputStream;
public class SoundManagerExecutor implements ISoundManager{

	private Object pausedLock;
	private boolean paused;
	
	private final ExecutorService executor;

	public SoundManagerExecutor() {
		pausedLock = new Object();
		executor = new ThreadPoolExecutor(2, 32, 30000L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>(64), new SoundThreadPoolFactory());
	}
	
	private static class SoundThreadPoolFactory implements ThreadFactory {

		private final ThreadGroup group;
		private final AtomicInteger threadNumber = new AtomicInteger(1);
		private final String namePrefix;

		private SoundThreadPoolFactory() {
			SecurityManager s = System.getSecurityManager();
			group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
			namePrefix = "SoundThreadPoolFactory" + "-thread-";
		}

		public Thread newThread(Runnable r) {
			Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
			if (t.isDaemon())
				t.setDaemon(false);
			if (t.getPriority() != Thread.NORM_PRIORITY)
				t.setPriority(Thread.NORM_PRIORITY);
			return t;
		}
	}
	public static int getMaxSimultaneousSounds(AudioFormat playbackFormat) {
		DataLine.Info lineInfo = new DataLine.Info(SourceDataLine.class, playbackFormat);
		Mixer mixer = AudioSystem.getMixer(null);
		return mixer.getMaxLines(lineInfo);
	}
	protected void cleanUp() {
		// signal to unpause
		setPaused(false);

		// close the mixer (stops any running sounds)
		Mixer mixer = AudioSystem.getMixer(null);
		if (mixer.isOpen()) {
			mixer.close();
		}
	}

	@Override
	public void close() {
		cleanUp();
		executor.shutdown();
	}

	public void setPaused(boolean paused) {
		if (this.paused != paused) {
			synchronized (pausedLock) {
				this.paused = paused;
				if (!paused) {
					pausedLock.notifyAll();
				}
			}
		}
	}

	public boolean isPaused() {
		return paused;
	}

	@Override
	public Sound getSound(String filename) {
		try {
			return getSound(getAudioInputStream(filename));
		} catch (UnsupportedAudioFileException e) {
			e.printStackTrace();
			return Sound.NONE_SOUND;
		}
	}
	
	@Override
	public Sound tryGetSound(String filename) throws UnsupportedAudioFileException {
		return getSound(getClass().getResourceAsStream(filename));
	}

	public Sound getSound(InputStream is) throws UnsupportedAudioFileException {
		return getSound(getAudioInputStream(is));
	}

	public Sound getSound(AudioInputStream audioStream) {
		if (audioStream == null) {
			return null;
		}

		int length = (int) (audioStream.getFrameLength() * audioStream.getFormat().getFrameSize());

		byte[] samples = new byte[length];
		DataInputStream is = new DataInputStream(audioStream);
		try {
			is.readFully(samples);
			is.close();
		} catch (IOException ex) {
			ex.printStackTrace();
		}

		return new Sound(samples, audioStream.getFormat());
	}

	public AudioInputStream getAudioInputStream(String filename) throws UnsupportedAudioFileException {
		return getAudioInputStream(getClass().getResourceAsStream(filename));
	}

	public AudioInputStream getAudioInputStream(InputStream is) throws UnsupportedAudioFileException {
		try {
			if (!is.markSupported()) {
				is = new BufferedInputStream(is);
			}
			AudioInputStream sourceAudio = AudioSystem.getAudioInputStream(is);
			return sourceAudio;
		} catch (IOException ex) {
			ex.printStackTrace();
		} catch (IllegalArgumentException ex) {
			ex.printStackTrace();
		}
		return null;
	}

	@Override
	public InputStream play(Sound sound) {
		return play(sound, null, false);
	}

	@Override
	public InputStream play(Sound sound, SoundFilter filter, boolean loop) {
		if(sound == null || !sound.isCanPlay()){
			return null;
		}
		InputStream is;
		if (loop) {
			is = new LoopingByteInputStream(sound.getSamples());
		} else {
			is = new ByteArrayInputStream(sound.getSamples());
		}
		return play(is, filter, sound.getFormat());
	}
	@Deprecated
	public InputStream play(InputStream is) {
		return play(is, null, null);
	}
	public InputStream play(InputStream is, SoundFilter filter, AudioFormat format) {
		if (is != null) {
			if (filter != null) {
				is = new FilteredSoundStream(is, filter);
			}
			runTask(new SoundPlayerTask(is, format));
		}
		return is;
	}
	
	private void runTask(Runnable task){
		executor.execute(task);
	}
	
	protected class SoundPlayerTask implements Runnable {

		private final InputStream source;
		private final AudioFormat playbackFormat;
		
		private SourceDataLine line;
		private byte[] buffer;

		public SoundPlayerTask(InputStream source, AudioFormat playbackFormat) {
			this.source = source;
			this.playbackFormat = playbackFormat;
		}
		
		private void init() {
			int bufferSize = playbackFormat.getFrameSize() * Math.round(playbackFormat.getSampleRate() / 10);
			SourceDataLine line;
			DataLine.Info lineInfo = new DataLine.Info(SourceDataLine.class, playbackFormat);
			try {
				line = (SourceDataLine) AudioSystem.getLine(lineInfo);
				line.open(playbackFormat, bufferSize);
			} catch (LineUnavailableException ex) {
				Thread.currentThread().interrupt();
				return;
			}

			line.start();
			byte[] buffer = new byte[bufferSize];
			this.line = line;
			this.buffer = buffer;
		}

		public void run() {
			init();
			
			if (line == null || buffer == null) {
				return;
			}
			try {
				int numBytesRead = 0;
				while (numBytesRead != -1) {
					synchronized (pausedLock) {
						if (paused) {
							try {
								pausedLock.wait();
							} catch (InterruptedException ex) {
								return;
							}
						}
					}
					numBytesRead = source.read(buffer, 0, buffer.length);
					if (numBytesRead != -1) {
						line.write(buffer, 0, numBytesRead);
					}
				}
			} catch (IOException ex) {
				ex.printStackTrace();
			}

		}
	}

}
